package com.cgq.service.impl;

import com.cgq.mapper.PhoneMapper;
import com.cgq.service.PhoneService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * *  Author:陈国强
 * *  Description:手机库存的实现类
 * *  Date:2023/2/14 19:55
 * 什么是分布式锁？
 * 原来的单体架构 演变成分布式架构  会出现一个问题 就是一个应用
 * 可能会部署到多个服务器  也就是集群，在分布式场景下
 * 原来单机部署所使用的并发控制  本地锁 synchronized 和 Lock 就会失效，
 * 因为本地锁只能管住一个jvm进程， 咱们
 * 需要一个 能跨jvm进程的互斥锁   来控制 共享资源的访问。
 *
 * 分布式锁的特征：
 *
 * 1.互斥性  在任意时刻 只能有一个线程占坑成功
 * 2.不能出现死锁  即使 线程没有执行解锁操作  也要保证后续的线程可以占有所
 * 3.不能释放别的线程的锁   解铃还须系铃人   加锁和解锁必须是同一个线程
 * 4.加锁 和 解锁必须具备原子性
 * 5.还需要考虑 锁的过期时间 与 业务的执行时间 长短的问题  理论上  锁的过期时间
 * 要大于业务的执行时间
 *
 * 手写的分布式锁
 *  优化点：
 *    给锁设置过期时间 避免死锁  这个操作需要具备原子性
 *    设置uuid 防误删
 *    使用lua 保证删除操作的原子性
 *
 *
 * 当前主流的分布式锁解决方案
 * 1. 基于redis解决方案       redisson
 * 2. 基于zookeeper 的 解决方案
 * 3. 基于数据库解决
 */
@Service
public class PhoneServiceImpl implements PhoneService {
    //Juc下的lock锁，设置为全局变量
    ReentrantLock lock = new ReentrantLock();

    @Resource
    private PhoneMapper mapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //普通的synchronized锁
    @Override
    public String getNumberById(Integer id) {
        //查询id商品的库存
        int number = mapper.getNumberById(id);

        //库存量小于等于0的情况，加上此条件，则只有一个服务器会进行购买成功
//        if (number <= 0){
//            return "很遗憾,你没有抢到该商品";
//        }

        if (number > 0){

            //上锁
            synchronized (this){
                number = mapper.getNumberById(id);
                if (number > 0){

                    //睡眠5秒
                    try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //购买后的商品库存受影响行数的变化
                    int result = mapper.updateNumberById(id);
                    if (result == 1){
                        return "恭喜你,抢到了该商品";
                    }
                }
            }

        }
        return "很遗憾,你没有抢到该商品";
    }


    //Luc下的Lock锁
    @Override
    public String getNumberById1(Integer id) {
        //查询查询的id的商品的库存
        int number = mapper.getNumberById(id);

        //库存量小于等于0的情况
        if (number <= 0){
            return "很遗憾,你没有抢到该商品";
        }

        //Luc下的Lock锁，进行加锁
        lock.lock();

        //再次进行查询
        number = mapper.getNumberById(id);
        try {

            //库存量大于0的情况
            if (number > 0){
                //睡眠5秒
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //购买后的商品库存受影响行数的变化
                int result = mapper.updateNumberById(id);
                //受影响行数的变化为1
                if (result == 1){
                    return "恭喜你,抢到了该商品";
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            // 一定要释放锁
            lock.unlock();
        }
        return "很遗憾,你没有抢到该商品";
    }


    //手动创建分布式锁
    @Override
    public String getNumberById2(Integer id) {
        //查询查询的id的商品的库存
        int number = mapper.getNumberById(id);

        //库存量小于等于0的情况
        if (number <= 0){
            return "很遗憾,你没有抢到该商品";
        }
        //占坑 加锁
        //Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "phonelock");

        //优化1  有可能出现死锁问题    为了尽可能避免死锁 给这把锁 加上过期时间
        //但是也是线程不安全，因为不知道业务的时间
        //stringRedisTemplate.expire("lock",10,TimeUnit.SECONDS);

        //优化2 加锁 和 给锁设置过期时间 需要具备原子性
        //stringRedisTemplate.opsForValue().setIfAbsent("lock","phonelock",3,TimeUnit.SECONDS);

        //优化3  给每个线程的锁 的 value 设置为 专属于这个线程的 value 这样在删除的时候 就可以
        //判断 当前线程要删除的锁 是不是 属于当前线程的锁
        String lockKey ="lock";
        String lockValue = UUID.randomUUID().toString();

        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent
                (lockKey,lockValue,3,TimeUnit.SECONDS);


        try {
            if (lock){
                //此时利用redis中的setnx进行加锁，如果lock为true，则redis中没有缓存的存在
                number = mapper.getNumberById(id);
                if (number > 0){

                    TimeUnit.SECONDS.sleep(2);

                    //购买后的商品库存
                    int result = mapper.updateNumberById(id);
                    if (result == 1){
                        return "恭喜你,抢到了该商品";
                    }else{
                        return "很遗憾,你没有抢到该商品";
                    }
                }
            }else{
                //进行自旋占坑
                while (true){
                    lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "phonelock");
                    if(lock){
                        number = mapper.getNumberById(id);
                        if(number>0){
                            int result = mapper.updateNumberById(id);

                            if(result==1){
                                return "恭喜你,抢到了该商品";
                            }
                        }else{
                            return "很遗憾,你没有抢到该商品";
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //释放锁   一定要释放锁 否则会出现死锁，未优化前的代码
            //stringRedisTemplate.delete("lock");

            //释放锁的时候 要进行判断 要删除属于自己的锁
//            if(lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))){
//                //进入 if 说明 是自己的锁
//                //随后进行释放锁
//                stringRedisTemplate.delete(lockKey);
//            }

            //继续 优化 上面的删除操作 不具备原子性  在将要执行删除操作时
            // cpu 切换到别的线程 别的线程抢占到锁
            //cpu 再切换回来 删除的就是 别的线程的锁 这种情况肯定是错误的
            // 所以 删除操作 的  判断 和删除两个动作  需要具备原子性  怎么解决
            // 使用 lua 脚本
            String script = "if redis.call('get',KEYS[1] ) == ARGV[1] " +
                    "then " +
                    "return redis.call('del',KEYS[1]) " +
                    "else " +
                    "return 0 end";
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

            redisScript.setScriptText(script);
            redisScript.setResultType(Long.class);

            Long execute = stringRedisTemplate.execute(redisScript, Arrays.asList(lockKey), lockValue);
            System.out.println(execute);
        }
        return "很遗憾,你没有抢到该商品";
    }


    @Autowired
    private RedissonClient redissonClient;
    //利用redisson分布式锁解决
    @Override
    public String getNumberById3(Integer id) {
        //查询查询的id的商品的库存
        int number = mapper.getNumberById(id);

        //库存量小于等于0的情况
        if (number <= 0){
            return "很遗憾,你没有抢到该商品";
        }
        RLock lock = redissonClient.getLock("redisson-lock");

        lock.lock();  // 占坑思想    看门狗机制  会根据业务的时间 来 延长锁的时间
        // 锁的时间默认30  执行到20 还未执行完 自动把锁续时到30

        lock.lock(5,TimeUnit.SECONDS); // 加一个5 秒的 锁   真正开发中 业务不能太久
        number = mapper.getNumberById(id);
        try {
            if(number>0){

                TimeUnit.SECONDS.sleep(1);

                int result = mapper.updateNumberById(id);

                if(result==1){
                    return "恭喜你，买到了";
                }
            }else{
                return "对不起,已售罄";
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放锁   一定要释放锁 否则会出现死锁，未优化前的代码
            lock.unlock();
        }
        return "对不起,已售罄";
    }
}
